package com.cdk8s.example.simplespringboot.utils;

import javax.net.ssl.X509TrustManager;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.*;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

@Slf4j
public class HttpClientUtil {
    private static IdleConnectionMonitorThread idleConnectionMonitorThread;

    private static final int IDLE_CHECK_GAP = 6;// 空闲连接监测，单位秒

    private static final int POOL_MAX_TOTAL = 200;// 最大的连接数

    private static final int POOL_DEFAULT_MAX_PER_ROUTE = 50;// 每路由（url）最高并发数，具体依据业务来定（同一个 url 最高并发数由此参数控制，所有 url 的最大并发数由 POOL_MAX_TOTAL 控制）

    private static RequestConfig requestConfig = null;

    private static final long KEEP_ALIVE_DURATION = 60;// 长连接的保持时长， 单位秒

    private static final int SOCKET_TIMEOUT = 10000;// 建立连接后，客户端从服务器读取数据的超时时长， 单位ms

    private static final int CONNECT_TIMEOUT = 5000;// 客户端和服务器建立连接的超时时长， 单位ms

    private static final int REQUEST_TIMEOUT = 3000;// 从连接池获取连接的超时时长， 单位ms

    private static final int MAX_HTTP_RETRY_COUNT = 1;

    // 单例：全局的池化HTTP客户端实例
    private static CloseableHttpClient pooledHttpClient;

    // 单例：HTTP长连接管理器(连接池)
    private static PoolingHttpClientConnectionManager httpClientConnectionManager;

    public static ContentType TEXT_XML_UTF8 = ContentType.create("text/xml", Consts.UTF_8);

    public static ContentType APPLICATION_JSON_UTF8 = ContentType.create("application/json", Consts.UTF_8);

    static {
        init();
    }

    public static void init() throws RuntimeException {
        try {
            SSLContextBuilder sslContextBuilder = new SSLContextBuilder();
            sslContextBuilder.loadTrustMaterial(null, new TrustAnyTrustManager());
            SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContextBuilder.build(), SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

            Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory> create().register("http", new PlainConnectionSocketFactory()).register("https", sslConnectionSocketFactory).build();

            // 长连接配置，如果没有约定，则默认定义时长为60s
            ConnectionKeepAliveStrategy keepAliveStrategy = new ConnectionKeepAliveStrategy() {
                @Override
                public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
                    HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
                    while (it.hasNext()) {
                        HeaderElement he = it.nextElement();
                        String param = he.getName();
                        String value = he.getValue();
                        if (value != null && param.equalsIgnoreCase("timeout")) {
                            return Long.parseLong(value) * 1000;
                        }
                    }
                    // 如果没有约定，则默认定义时长为60s
                    return KEEP_ALIVE_DURATION * 1000;
                }
            };

            httpClientConnectionManager = new PoolingHttpClientConnectionManager(registry);
            httpClientConnectionManager.setMaxTotal(POOL_MAX_TOTAL);
            httpClientConnectionManager.setDefaultMaxPerRoute(POOL_DEFAULT_MAX_PER_ROUTE);
            requestConfig = RequestConfig.custom().setConnectionRequestTimeout(REQUEST_TIMEOUT).setConnectTimeout(CONNECT_TIMEOUT).setSocketTimeout(SOCKET_TIMEOUT).build();

            HttpClientBuilder httpClientBuilder = HttpClients.custom();
            httpClientBuilder.setConnectionManager(httpClientConnectionManager);
            httpClientBuilder.setKeepAliveStrategy(keepAliveStrategy);
            httpClientBuilder.setDefaultRequestConfig(requestConfig);

            // 第一种重试方式：
            // ServiceUnavailableRetryStrategy serviceUnavailableRetryStrategy = new ServiceUnavailableRetryStrategy() {
            // /**
            // * retry逻辑
            // */
            // @Override
            // public boolean retryRequest(HttpResponse response, int executionCount, HttpContext context) {
            // if (executionCount >= MAX_HTTP_RETRY_COUNT)
            // return false;
            // else {
            // return true;
            // }
            // }
            //
            // /**
            // * retry间隔时间
            // */
            // @Override
            // public long getRetryInterval() {
            // return 2000;
            // }
            // };
            // httpClientBuilder.setServiceUnavailableRetryStrategy(serviceUnavailableRetryStrategy);

            // 第二种重试方式
            // HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() {
            // @Override
            // public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
            // if (executionCount >= MAX_HTTP_RETRY_COUNT) {
            // return false;
            // }
            // if (exception instanceof InterruptedIOException) {
            // // Timeout
            // log.warn("httpUtil retry for InterruptIOException");
            // return true;
            // }
            // if (exception instanceof UnknownHostException) {
            // // Unknown host
            // return false;
            // }
            // if (exception instanceof SSLException) {
            // // SSL handshake exception
            // return false;
            // }
            // HttpClientContext clientContext = HttpClientContext.adapt(context);
            // HttpRequest request = clientContext.getRequest();
            // boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
            // if (idempotent) {
            // // Retry if the request is considered idempotent
            // log.warn("httpUtil retry for idempotent");
            // return true;
            // }
            // return false;
            // }
            // };
            // httpClientBuilder.setRetryHandler(httpRequestRetryHandler);

            pooledHttpClient = httpClientBuilder.build();

            // 当一个连接释放回管理器，它会保持活跃，然后它不会监听socket的状态和任何IO事件。如果这个连接在服务器端被关闭，客户端的连接在连接状态下不会检测出这个改变。
            // 所以我们需要有一个专门的监听线程是被用来驱逐已经过期不活跃的长连接的。

            // 定时关闭连接池空闲连接
            idleConnectionMonitorThread = new IdleConnectionMonitorThread(httpClientConnectionManager);
            idleConnectionMonitorThread.start();

            // 通过线程池，定时关闭连接池空闲连接（另一种方式）
            // startExpiredConnectionsMonitor();

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    // 使用单个线程进行清理空闲连接
    public static class IdleConnectionMonitorThread extends Thread {

        private final HttpClientConnectionManager connMgr;

        private volatile boolean shutdown;

        public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
            super();
            this.connMgr = connMgr;
        }

        @Override
        public void run() {
            try {
                while (!shutdown) {
                    synchronized (this) {
                        wait(5000);// 每5秒处理一次
                        connMgr.closeExpiredConnections();
                        connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
                    }
                }
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }

        public void shutdown() {
            shutdown = true;
            synchronized (this) {
                notifyAll();
            }
        }

    }

    /**
     * 关闭定时线程.
     */
    public synchronized static void shutdown() {
        idleConnectionMonitorThread.shutdown();
    }

    /**
     * 定时处理线程：对异常连接进行关闭（另一种方式）
     */
    // private static void startExpiredConnectionsMonitor() {
    // // 开启监控线程,对异常和空闲线程进行关闭
    // ScheduledExecutorService monitorExecutor = Executors.newScheduledThreadPool(1);
    // monitorExecutor.scheduleAtFixedRate(new TimerTask() {
    // @Override
    // public void run() {
    // // 关闭异常连接
    // httpClientConnectionManager.closeExpiredConnections();
    // // 关闭keepAliveTimeout（保持连接时长）超时的不活跃的连接
    // httpClientConnectionManager.closeIdleConnections(KEEP_ALIVE_DURATION, TimeUnit.SECONDS);
    // // 获取连接池的状态
    // PoolStats status = httpClientConnectionManager.getTotalStats();
    // }
    // }, IDLE_CHECK_GAP, IDLE_CHECK_GAP, TimeUnit.SECONDS);
    // httpClientConnectionManager.closeExpiredConnections();
    // httpClientConnectionManager.closeIdleConnections(KEEP_ALIVE_DURATION, TimeUnit.SECONDS);
    // }

    private static class TrustAnyTrustManager implements X509TrustManager, TrustStrategy {
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[] {};
        }

        @Override
        public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            return true;
        }
    }

    public static CloseableHttpClient getPooledHttpClient() {
        if (null == pooledHttpClient) {
            init();
        }
        return pooledHttpClient;
    }

    public static String getWithResult(String url) {
        return getWithResult(url, null);
    }

    public static String getByUrlQueryMapWithResult(String url, Map<String, String> queryMap) {
        StringBuilder fullUrl = new StringBuilder(url);
        if (queryMap != null && queryMap.keySet().size() > 0) {
            fullUrl.append("?");

            for (Map.Entry<String, String> entry : queryMap.entrySet()) {
                if (StringUtil.isNotBlank(entry.getValue()) && !StringUtil.equalsIgnoreCase(entry.getValue(), "null")) {
                    fullUrl.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
                }
            }

            fullUrl.deleteCharAt(fullUrl.length() - 1);
        }
        return getWithResult(fullUrl.toString(), null);
    }

    public static String getWithResult(String url, Map<String, String> headerMap) {
        CloseableHttpClient client = getPooledHttpClient();
        HttpGet httpGet = null;
        int resultCode = 0;
        try {
            httpGet = new HttpGet(completeUrl(url));
            if (null != headerMap) {
                for (Map.Entry<String, String> entry : headerMap.entrySet()) {
                    httpGet.addHeader(entry.getKey(), entry.getValue());
                }
            }
            CloseableHttpResponse response = client.execute(httpGet);
            resultCode = response.getStatusLine().getStatusCode();
            if (resultCode == HttpStatus.SC_OK) {
                HttpEntity responseEntity = response.getEntity();
                return EntityUtils.toString(responseEntity, Consts.UTF_8);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != httpGet) {
                // 归还连接到到连接池
                httpGet.releaseConnection();
            }
        }
        return null;
    }

    @SneakyThrows
    public static String postFormWithResult(String url, Map<String, Object> params, Map<String, String> headerMap) {
        CloseableHttpClient client = getPooledHttpClient();
        HttpPost httpPost = null;
        int resultCode = 0;
        try {

            List<NameValuePair> formParams = new ArrayList<>();
            params.forEach((k, v) -> {
                if (v != null) {
                    formParams.add(new BasicNameValuePair(k, v.toString()));
                }
            });
            HttpEntity entity = new UrlEncodedFormEntity(formParams, StandardCharsets.UTF_8);
            httpPost = new HttpPost(completeUrl(url));
            httpPost.setEntity(entity);

            if (null != headerMap) {
                Set<String> headerKeys = headerMap.keySet();
                for (String k : headerKeys) {
                    httpPost.setHeader(k, headerMap.get(k));
                }
            }

            CloseableHttpResponse response = client.execute(httpPost);
            resultCode = response.getStatusLine().getStatusCode();
            if (resultCode == HttpStatus.SC_OK) {
                HttpEntity responseEntity = response.getEntity();
                return EntityUtils.toString(responseEntity, Consts.UTF_8);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != httpPost) {
                // 归还连接到到连接池
                httpPost.releaseConnection();
            }
        }
        return null;
    }

    public static String postJsonWithResult(String url, String body, Map<String, String> headerMap) {
        CloseableHttpClient client = getPooledHttpClient();
        HttpPost httpPost = null;
        int resultCode = 0;
        try {
            httpPost = new HttpPost(completeUrl(url));
            if (StringUtils.isNotBlank(body)) {
                HttpEntity httpEntity = new StringEntity(body, APPLICATION_JSON_UTF8);
                httpPost.setEntity(httpEntity);
            }
            httpPost.setHeader("Accept", "application/json");
            httpPost.setHeader("Content-Type", "application/json");

            if (null != headerMap) {
                Set<String> headerKeys = headerMap.keySet();
                for (String k : headerKeys) {
                    httpPost.setHeader(k, headerMap.get(k));
                }
            }

            CloseableHttpResponse response = client.execute(httpPost);
            resultCode = response.getStatusLine().getStatusCode();
            if (resultCode == HttpStatus.SC_OK) {
                HttpEntity responseEntity = response.getEntity();
                return EntityUtils.toString(responseEntity, Consts.UTF_8);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != httpPost) {
                // 归还连接到到连接池
                httpPost.releaseConnection();
            }
        }
        return null;
    }

    private static String completeUrl(String oldUrl) {
        if (StringUtils.isBlank(oldUrl)) {
            return StringUtils.EMPTY;
        }
        if (!oldUrl.startsWith("http")) {
            oldUrl = "http://" + oldUrl;
        }
        return oldUrl;

    }
}
